// Updated NextAuth configuration with dynamic session timeout from database import NextAuth, { NextAuthOptions, Session, User, Account } from 'next-auth' import { JWT } from "next-auth/jwt" import CredentialsProvider from 'next-auth/providers/credentials' import { SAMLProvider } from './saml/provider' import { getUserById } from '@/lib/users/repository' import { authenticateWithSGips, verifyExternalCredentials } from '@/lib/users/auth/verifyCredentails' import { verifyOtpTemp } from '@/lib/users/verifyOtp' import { getSecuritySettings } from '@/lib/password-policy/service' // 인증 방식 타입 정의 type AuthMethod = 'otp' | 'email' | 'sgips' | 'saml' // 모듈 보강 선언 (인증 방식 추가) declare module "next-auth" { interface Session { user: { id: string name?: string | null email?: string | null image?: string | null companyId?: number | null techCompanyId?: number | null domain?: string | null reAuthTime?: number | null authMethod?: AuthMethod sessionExpiredAt?: number | null // 세션 만료 시간 추가 } } interface User { id: string imageUrl?: string | null companyId?: number | null techCompanyId?: number | null domain?: string | null reAuthTime?: number | null authMethod?: AuthMethod } } declare module "next-auth/jwt" { interface JWT { id?: string imageUrl?: string | null companyId?: number | null techCompanyId?: number | null domain?: string | null reAuthTime?: number | null authMethod?: AuthMethod sessionExpiredAt?: number | null // 세션 만료 시간 추가 } } // 보안 설정 캐시 (성능 최적화) let securitySettingsCache: { data: any | null lastFetch: number ttl: number } = { data: null, lastFetch: 0, ttl: 5 * 60 * 1000 // 5분 캐시 } // 보안 설정을 가져오는 함수 (캐시 적용) async function getCachedSecuritySettings() { const now = Date.now() if (!securitySettingsCache.data || (now - securitySettingsCache.lastFetch) > securitySettingsCache.ttl) { try { securitySettingsCache.data = await getSecuritySettings() securitySettingsCache.lastFetch = now } catch (error) { console.error('Failed to fetch security settings:', error) // 기본값 사용 securitySettingsCache.data = { sessionTimeoutMinutes: 480 // 8시간 기본값 } } } return securitySettingsCache.data } export const authOptions: NextAuthOptions = { providers: [ // OTP provider CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'text' }, code: { label: 'OTP code', type: 'text' }, }, async authorize(credentials, req) { const { email, code } = credentials ?? {} const user = await verifyOtpTemp(email ?? '') if (!user) { return null } // 보안 설정에서 세션 타임아웃 가져오기 const securitySettings = await getCachedSecuritySettings() const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000 const reAuthTime = Date.now() return { id: String(user.id ?? email ?? "dts"), email: user.email, imageUrl: user.imageUrl ?? null, name: user.name, companyId: user.companyId, techCompanyId: user.techCompanyId as number | undefined, domain: user.domain, reAuthTime, authMethod: 'otp' as AuthMethod, } }, }), // ID/패스워드 provider (S-Gips와 일반 이메일 구분) CredentialsProvider({ id: 'credentials-password', name: 'Username Password', credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" }, provider: { label: "Provider", type: "text" }, }, async authorize(credentials, req) { if (!credentials?.username || !credentials?.password) { return null; } try { let authResult; const isSSgips = credentials.provider === 'sgips'; if (isSSgips) { authResult = await authenticateWithSGips( credentials.username, credentials.password ); } else { authResult = await verifyExternalCredentials( credentials.username, credentials.password ); } if (authResult.success && authResult.user) { return { id: authResult.user.id, name: authResult.user.name, email: authResult.user.email, imageUrl: authResult.user.imageUrl ?? null, companyId: authResult.user.companyId, techCompanyId: authResult.user.techCompanyId, domain: authResult.user.domain, reAuthTime: Date.now(), authMethod: isSSgips ? 'sgips' as AuthMethod : 'email' as AuthMethod, }; } return null; } catch (error) { console.error("Authentication error:", error); return null; } } }), // SAML Provider SAMLProvider({ id: "credentials-saml", name: "SAML SSO", idp: { sso_login_url: process.env.SAML_IDP_SSO_URL!, sso_logout_url: process.env.SAML_IDP_SLO_URL || '', certificates: [process.env.SAML_IDP_CERT!] }, sp: { entity_id: process.env.SAML_SP_ENTITY_ID!, private_key: process.env.SAML_SP_PRIVATE_KEY || '', certificate: process.env.SAML_SP_CERT || '', assert_endpoint: process.env.SAML_SP_CALLBACK_URL || `${process.env.NEXTAUTH_URL}/api/saml/callback` } }) ], session: { strategy: 'jwt', // JWT 기본 maxAge는 30일로 설정하되, 실제 세션 만료는 콜백에서 처리 maxAge: 30 * 24 * 60 * 60, // 30일 }, callbacks: { // JWT 콜백 - 세션 타임아웃 설정 (만료 체크는 session 콜백에서) async jwt({ token, user, account, trigger, session }) { // 보안 설정 가져오기 const securitySettings = await getCachedSecuritySettings() const sessionTimeoutMs = securitySettings.sessionTimeoutMinutes * 60 * 1000 // 최초 로그인 시 if (user) { const reAuthTime = Date.now() token.id = user.id token.email = user.email token.name = user.name token.companyId = user.companyId token.techCompanyId = user.techCompanyId token.domain = user.domain token.imageUrl = user.imageUrl token.reAuthTime = reAuthTime token.authMethod = user.authMethod token.sessionExpiredAt = reAuthTime + sessionTimeoutMs } // 인증 방식 결정 (account 정보 기반) if (account && !token.authMethod) { const reAuthTime = Date.now() if (account.provider === 'credentials-saml') { token.authMethod = 'saml' token.reAuthTime = reAuthTime token.sessionExpiredAt = reAuthTime + sessionTimeoutMs } else if (account.provider === 'credentials') { // OTP는 이미 user.authMethod에서 설정됨 if (!token.sessionExpiredAt) { token.sessionExpiredAt = (token.reAuthTime || Date.now()) + sessionTimeoutMs } } else if (account.provider === 'credentials-password') { // credentials-password는 이미 user.authMethod에서 설정됨 if (!token.sessionExpiredAt) { token.sessionExpiredAt = (token.reAuthTime || Date.now()) + sessionTimeoutMs } } } // 세션 업데이트 시 (재인증 시간 업데이트) if (trigger === "update" && session) { if (session.reAuthTime !== undefined) { token.reAuthTime = session.reAuthTime // 재인증 시간 업데이트 시 세션 만료 시간도 연장 token.sessionExpiredAt = session.reAuthTime + sessionTimeoutMs } if (session.user) { if (session.user.name !== undefined) token.name = session.user.name if (session.user.email !== undefined) token.email = session.user.email if (session.user.image !== undefined) token.imageUrl = session.user.image } } return token }, // Session 콜백 - 세션 만료 체크 및 정보 포함 async session({ session, token }: { session: Session; token: JWT }) { // 세션 만료 체크 if (token.sessionExpiredAt && Date.now() > token.sessionExpiredAt) { console.log(`Session expired for user ${token.email}. Expired at: ${new Date(token.sessionExpiredAt)}`) // 만료된 세션 처리 - 빈 세션 반환하여 로그아웃 유도 return { expires: new Date(0).toISOString(), // 즉시 만료 user: null as any } } if (token) { session.user = { id: token.id as string, email: token.email as string, name: token.name as string, domain: token.domain as string, companyId: token.companyId as number, techCompanyId: token.techCompanyId as number, image: token.imageUrl ?? null, reAuthTime: token.reAuthTime as number | null, authMethod: token.authMethod as AuthMethod, sessionExpiredAt: token.sessionExpiredAt as number | null, } } return session }, // Redirect 콜백 async redirect({ url, baseUrl }) { if (url.startsWith("/")) { return `${baseUrl}${url}`; } else if (new URL(url).origin === baseUrl) { return url; } return baseUrl; }, }, pages: { signIn: '/auth/login', error: '/auth/error', }, // 디버깅을 위한 이벤트 로깅 events: { async signIn({ user, account, profile }) { const securitySettings = await getCachedSecuritySettings() console.log(`User ${user.email} signed in via ${account?.provider} (authMethod: ${user.authMethod}), session timeout: ${securitySettings.sessionTimeoutMinutes} minutes`); }, async signOut({ session, token }) { console.log(`User ${session?.user?.email || token?.email} signed out`); } } } const handler = NextAuth(authOptions) export { handler as GET, handler as POST }